package filodb.prometheus.parse

import com.typesafe.scalalogging.StrictLogging

import filodb.core.GlobalConfig
import filodb.core.query.{ColumnFilter, Filter, QueryConfig, QueryUtils}
import filodb.prometheus.ast._
import filodb.query.{LabelNames, LabelValues, LogicalPlan}

/**
  * Parser routes requests to LegacyParser or AntlrParser.
  */
object Parser extends StrictLogging {

  val REGEX_MAX_LEN = 1000

  sealed trait Mode
  case object Legacy extends Mode
  case object Antlr extends Mode
  case object Shadow extends Mode

  private val conf = GlobalConfig.systemConfig
  private val queryConf = conf.getConfig("filodb.query")

  val mode: Mode = {
    val queryConfig = QueryConfig(queryConf)
    val parser = queryConfig.parser
    logger.info(s"Query parser mode: $parser")
    parser match {
      case "legacy" => Legacy
      case "antlr" => Antlr
      case "shadow" => Shadow
    }
  }

  // Only called directly by tests.
  def parseQuery(query: String): Expression = {
    mode match {
      case Antlr => AntlrParser.parseQuery(query)
      case Legacy => LegacyParser.parseQuery(query)
      case Shadow => {
        val expr = LegacyParser.parseQuery(query)
        try {
          AntlrParser.parseQuery(query)
        } catch {
          case e: Throwable => {
            logger.error(s"Antlr parse error: $query", e)
          }
        }
        expr
      }
    }
  }

  // TODO: Once fully switched to AntlrParser, get rid of the special precedence methods.
  private def parseQueryWithPrecedence(query: String, modeParam: Mode = mode): Expression = {
    modeParam match {
      case Antlr => AntlrParser.parseQuery(query)
      case Legacy => LegacyParser.parseQueryWithPrecedence(query)
      case Shadow => {
        val expr = LegacyParser.parseQueryWithPrecedence(query)
        try {
          val antlrExpr = AntlrParser.parseQuery(query)
          if (!expr.equals(antlrExpr))  logger.error(s"Antlr parsed expression does not match expression " +
            s"generated by Legacy Parser for query: $query")
        } catch {
          case e: Throwable => {
            logger.error(s"Antlr parse error: $query", e)
          }
        }
        expr
      }
    }
  }

  // Only called by tests.
  def parseLabelValueFilter(query: String): Seq[LabelMatch] = {
    mode match {
      case Antlr => AntlrParser.parseLabelValueFilter(query)
      case Legacy => LegacyParser.parseLabelValueFilter(query)
      case Shadow => {
        val labels = LegacyParser.parseLabelValueFilter(query)
        try {
          AntlrParser.parseLabelValueFilter(query)
        } catch {
          case e: Throwable => {
            logger.error(s"Antlr parse error: $query", e)
          }
        }
        labels
      }
    }
  }

  def parseFilter(query: String): InstantExpression = {
    val expr = parseQuery(query)
    if (expr.isInstanceOf[InstantExpression]) {
      expr.asInstanceOf[InstantExpression]
    } else {
      throw new IllegalArgumentException(s"Expression $query is not a simple filter")
    }
  }

  def metadataQueryToLogicalPlan(query: String, timeParams: TimeRangeParams,
                                 fetchFirstLastSampleTimes: Boolean = false): LogicalPlan = {
    val expression = parseQuery(query)
    expression match {
      case p: InstantExpression => p.toMetadataPlan(timeParams, fetchFirstLastSampleTimes)
      case _ => throw new UnsupportedOperationException()
    }
  }

  /**
   * Returns a mapping from selector labels (including an explicit/implicit __name__) to values.
   * A metric name need not be present in the query.
   *
   * @param query: must specify label values by equality without a regex
   */
  def queryToEqualLabelMap(query: String): Map[String, String] = {
    val expression = parseQuery(query)
    expression match {
      case p: InstantExpression => p.toEqualLabelMap()
      case _ => throw new UnsupportedOperationException()
    }
  }

  def labelCardinalityToLogicalPlan(query: String, timeParams: TimeRangeParams): LogicalPlan = parseQuery(query) match {
      case p: InstantExpression => p.toLabelCardinalityPlan(timeParams)
      case _ => throw new UnsupportedOperationException()
  }

  // Only called by tests.
  def labelValuesQueryToLogicalPlan(labelNames: Seq[String], filterQuery: Option[String],
                                    timeParams: TimeRangeParams): LogicalPlan = {
    filterQuery match {
      case Some(filter) =>
        val columnFilters = parseLabelValueFilter(filter).map { l =>
          l.labelMatchOp match {
            case EqualMatch => ColumnFilter(l.label, Filter.Equals(l.value))
            case NotRegexMatch => require(l.value.length <= REGEX_MAX_LEN,
                                   s"Regular expression filters should be <= $REGEX_MAX_LEN characters")
                                  ColumnFilter(l.label, Filter.NotEqualsRegex(l.value))
            case RegexMatch    =>
              // Relax the length limit only for matchers that contain at most the "|" special character.
              val shouldRelax = queryConf.hasPath("relaxed-pipe-only-equals-regex-limit") &&
                                  QueryUtils.containsPipeOnlyRegex(l.value)
              if (shouldRelax) {
                val limit = queryConf.getInt("relaxed-pipe-only-equals-regex-limit");
                require(l.value.length <= limit,
                  s"Regular expression filters should be <= $limit characters " +
                    s"when no special characters except '|' are used. " +
                    s"Violating filter is: ${l.label}=${l.value}")
              } else {
                require(l.value.length <= Parser.REGEX_MAX_LEN,
                  s"Regular expression filters should be <= ${Parser.REGEX_MAX_LEN} characters " +
                   s"when non-`|` special characters are used. " +
                   s"Violating filter is: ${l.label}=${l.value}")
              }
              ColumnFilter(l.label, Filter.EqualsRegex(l.value))
            case NotEqual(false) => ColumnFilter(l.label, Filter.NotEquals(l.value))
            case other: Any => throw new IllegalArgumentException(s"Unknown match operator $other")
          }
        }
        LabelValues(labelNames, columnFilters, timeParams.start * 1000, timeParams.end * 1000)
      case _ =>
        LabelValues(labelNames, Seq.empty, timeParams.start * 1000, timeParams.end * 1000)
    }
  }

  def labelNamesQueryToLogicalPlan(query: String,
                                   timeParams: TimeRangeParams): LogicalPlan = {
    if (query == null || query.isEmpty) {
      LabelNames(Seq.empty, timeParams.start * 1000, timeParams.end * 1000)
    } else {
      val expression = parseQuery(query)
      expression match {
        case p: InstantExpression => p.toLabelNamesPlan(timeParams)
        case _ => throw new UnsupportedOperationException()
      }
    }
  }

  def queryToLogicalPlan(query: String, queryTimestamp: Long, step: Long, mode: Mode = mode): LogicalPlan = {
    // Remember step matters here in instant query, when lookback is provided in step factor
    // notation as in [5i]
    val defaultQueryParams = TimeStepParams(queryTimestamp, step, queryTimestamp)
    queryRangeToLogicalPlan(query, defaultQueryParams, mode)
  }

  def queryRangeToLogicalPlan(query: String, timeParams: TimeRangeParams, mode: Mode = mode): LogicalPlan = {
    val ex = parseQueryWithPrecedence(query, mode)
    ex match {
      case p: PeriodicSeries => p.toSeriesPlan(timeParams)
      case r: SimpleSeries   => r.toSeriesPlan(timeParams, isRoot = true)
      case _ => throw new UnsupportedOperationException()
    }
  }

  /**
   * Parses a single PromQL ColumnFilter.
   *
   * @param filter a PromQL-formatted filter. Example: foo=~"bar.*"
   */
  def parseColumnFilter(filter: String): ColumnFilter = {
    Parser.parseQuery(s"{$filter}")
      .asInstanceOf[InstantExpression]
      .getUnvalidatedColumnFilters()
      .head
  }
}

object ParserUtil {
  /**
   * Strip quotes and process escape codes.
   */
  def dequote(str: String): String = {
    val bob = new StringBuilder()
    var offset = 1
    while (offset < str.length() - 1) {
      var c = str.charAt(offset); offset += 1
      if (c == '\\') {
        val next = str.charAt(offset); offset += 1
        c = next match {
          case '\\' | '\'' | '"' | '.' => next
          case 'f' => '\f'
          case 'n' => '\n'
          case 'r' => '\r'
          case 't' => '\t'
          case _ => throw new IllegalArgumentException("illegal string escape: " + next)
        }
      }
      bob.append(c)
    }

    bob.toString()
  }
}
